import GCDWebServers
import Gzip
import JavaScriptCore
import PromiseKit
@testable import ServiceWorker
import XCTest

class FetchResponseTests: XCTestCase {
    override func setUp() {
        super.setUp()
        URLCache.shared.removeAllCachedResponses()
        TestWeb.createServer()
    }

    override func tearDown() {
        TestWeb.destroyServer()
        super.tearDown()
    }

    func testFetchResponseText() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.txt", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in
            let res = GCDWebServerDataResponse(text: "THIS IS TEST CONTENT")
            res!.statusCode = 200
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test.txt"))
            .then { res in
                res.text()
            }
            .map { str -> Void in
                XCTAssertEqual(str, "THIS IS TEST CONTENT")
            }
            .assertResolves()
    }

    func testGzipResponse() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test-gzip.txt", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in

            var res: GCDWebServerDataResponse?

            do {
                let gzipped = try "THIS IS TEST CONTENT".data(using: String.Encoding.utf8)!.gzipped()

                res = GCDWebServerDataResponse(data: gzipped, contentType: "text/plain")
                res!.setValue("gzip", forAdditionalHeader: "Content-Encoding")
                res!.statusCode = 200

            } catch {
                XCTFail(String(describing: error))
            }
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test-gzip.txt"))
            .then { response -> Promise<Void> in
                XCTAssertNil(response.headers.get("Content-Length"))

                return response.text()
                    .map { text -> Void in
                        XCTAssertEqual(text, "THIS IS TEST CONTENT")
                    }
            }.assertResolves()
    }

    func testFetchResponseJSON() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.json", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in
            let res = GCDWebServerDataResponse(jsonObject: [
                "test": "value"
            ])
            res!.statusCode = 200
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test.json"))
            .then { response in
                response.json()
            }
            .map { obj -> Void in

                let json = obj as! [String: Any]

                XCTAssertEqual(json["test"] as! String, "value")
            }
            .assertResolves()
    }

    func testResponseInWorker() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.txt", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in
            let res = GCDWebServerDataResponse(text: "THIS IS TEST CONTENT")
            res!.statusCode = 200
            return res
        }

        let sw = TestWorker(id: "TEST", state: .activated, url: TestWeb.serverURL, content: "")

        sw.evaluateScript("""
            fetch('\(TestWeb.serverURL.appendingPathComponent("/test.txt"))')
            .then(function(res) { return res.text() })
        """)
            .then { (val: JSContextPromise) in
                val.resolve()
            }
            .map { (val: String) in
                XCTAssertEqual(val, "THIS IS TEST CONTENT")
            }
            .assertResolves()
    }

    func testFetchResponseClone() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.txt", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in
            let res = GCDWebServerDataResponse(text: "THIS IS TEST CONTENT")
            res!.statusCode = 200
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test.txt"))
            .then { res -> Promise<Void> in

                let clone = try res.clone()

                let cloneText = clone.text()

                let originalText = res.text()

                return when(fulfilled: [originalText, cloneText])
                    .map { results -> Void in
                        XCTAssertEqual(results.count, 2)
                        results.forEach { XCTAssertEqual($0, "THIS IS TEST CONTENT") }
                    }
            }
            .assertResolves()
    }

    func testDataResponse() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.dat", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in

            let d = Data(bytes: [1, 2, 3, 4, 254])
            let res = GCDWebServerDataResponse(data: d, contentType: "application/binary")
            res.statusCode = 200
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test.dat"))
            .then { res in
                res.data()
            }
            .map { data -> Void in

                data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) in
                    XCTAssertEqual(bytes[0], 1)
                    XCTAssertEqual(bytes[1], 2)
                    XCTAssertEqual(bytes[2], 3)
                    XCTAssertEqual(bytes[3], 4)
                    XCTAssertEqual(bytes[4], 254)
                }
            }
            .assertResolves()
    }

    func testArrayBufferResponse() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.dat", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in

            let d = Data(bytes: [1, 2, 3, 4, 255])
            let res = GCDWebServerDataResponse(data: d, contentType: "application/binary")
            res.statusCode = 200
            return res
        }

        let sw = TestWorker(id: "TEST", state: .activated, url: TestWeb.serverURL, content: "")

        sw.evaluateScript("""
            fetch('\(TestWeb.serverURL.appendingPathComponent("/test.dat"))')
            .then(function(res) { return res.arrayBuffer() })
            .then(function(arrBuffer) {
                let arr = new Uint8Array(arrBuffer);
                return [arr[0], arr[1], arr[2], arr[3], arr[4]]
            })
        """)
            .then { (val: JSContextPromise) in
                val.resolve()
            }
            .map { (arr: [Int]) -> Void in

                XCTAssertEqual(arr[0], 1)
                XCTAssertEqual(arr[1], 2)
                XCTAssertEqual(arr[2], 3)
                XCTAssertEqual(arr[3], 4)
                XCTAssertEqual(arr[4], 255)
            }
            .assertResolves()
    }

    func testResponseToFileDownload() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.txt", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in
            let res = GCDWebServerDataResponse(text: "THIS IS TEST CONTENT")
            res!.statusCode = 200
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test.txt"))
            .then { res in
                res.internalResponse.fileDownload { localFile, _ in

                    // test that we can use async promises here

                    Promise<Void> { resolver in
                        DispatchQueue.global(qos: .background).async {
                            resolver.fulfill(())
                        }
                    }
                    .map { () -> String in
                        try String(contentsOfFile: localFile.path)
                    }
                }
            }
            .map { contents in
                XCTAssertEqual(contents, "THIS IS TEST CONTENT")
            }
            .assertResolves()
    }

    func testResponseToFileDownloadHandlesErrors() {
        TestWeb.server.addHandler(forMethod: "GET", path: "/test.txt", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in
            let res = GCDWebServerDataResponse(text: "THIS IS TEST CONTENT")
            res!.statusCode = 200
            return res
        }

        FetchSession.default.fetch(url: TestWeb.serverURL.appendingPathComponent("/test.txt"))
            .then { res in
                res.internalResponse.fileDownload { _, _ in

                    throw ErrorMessage("Oh no")
                }
            }
            .recover { error in
                XCTAssertEqual((error as? ErrorMessage)?.message, "Oh no")
            }
            .assertResolves()
    }
}
